package cn.t.keycloak.demo.mfa.sms;

import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.RandomUtil;
import cn.t.keycloak.demo.mfa.sms.action.SmsOptMfaAuthRequiredAction;
import cn.t.keycloak.demo.mfa.sms.credential.SmsCredentialModel;
import cn.t.keycloak.demo.mfa.sms.credential.SmsCredentialProvider;
import cn.t.keycloak.demo.mfa.sms.credential.SmsCredentialProviderFactory;
import cn.t.keycloak.demo.mfa.sms.spi.ISmsSenderProvider;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.authentication.*;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.models.*;

import java.util.List;
import java.util.Objects;

/**
 * 认证器,CredentialValidator接口允许获得凭证表单验证
 * https://www.keycloak.org/docs/latest/server_development/index.html#implementing-an-authenticatorfactory
 * @author 陶敏麒
 * @date 2023/11/15 15:34
 */
@Slf4j
public class SmsMfaAuthenticator implements Authenticator, CredentialValidator<SmsCredentialProvider> {


    public SmsMfaAuthenticator() {
        log.info("SmsAuthenticator手机验证码验证器初始化完成");
    }

    /**
     * 核心的认证逻辑
     * @param context 认证流程上下文
     */
    @Override
    public void authenticate(AuthenticationFlowContext context) {
        log.info(">>>>>>>>>>>>>>>>>>>>进入手机验证码流程");

        if (Objects.equals(context.getAuthenticationSession().getAuthNote(AuthConstants.SKIP_SMS_MFA), "1")) {
            log.info("当前用户[{}]已经使用手机号码登录,跳过手机验证码认证", context);
            context.success();
            return;
        }

        var opSmsCredential = SmsCredentialModel.queryCredentialModelByUser(context.getUser());
        if (opSmsCredential.isEmpty()) {
            log.warn("当前用户[{}]没有配置SMS验证码凭证", context.getUser().getUsername());
            // 跳回登录页面重新配置
            context.resetFlow();
            return;
        }
        var credential = SmsCredentialModel.createFromCredentialModel(opSmsCredential.get());

        // 需要提供一个验证码发送的SPI接口
        // 另外起一个Rest Spi即可,这里直接使用DemoRestProvider
        // 创建一个手机验证码输入表单
        String phoneNumber = credential.getSmsCredentialData().getPhoneNumber();
        var from = context.form().setAttribute("phoneNumber", DesensitizedUtil.mobilePhone(phoneNumber));
        // 给手机发验证码
        // 生成验证码
        String randomNumbers = RandomUtil.randomNumbers(6);
        log.info(">>>>开始向手机[{}]发送MFA认证的验证码[{}]", phoneNumber, randomNumbers);
        int expires = context.getSession().getProvider(ISmsSenderProvider.class).sendVerificationCode(phoneNumber, randomNumbers);
        from.setAttribute("expireIn", expires);
        Response challenge = from.createForm(SmsProperties.SMS_FORM_NAME);
        // 状态置为challenge
        context.challenge(challenge);
    }

    /**
     * 如果返回了用户界面(前端输入界面,这里是手机验证码登录,需要返回界面),用户输入提交后进入这个方法进行最后的认证
     * @param context 认证流程上下文
     */
    @Override
    public void action(AuthenticationFlowContext context) {
        // 校验验证码
        boolean validated = validateCode(context);
        if (!validated) {
            // 校验失败 返回前端页面
            Response challenge =  context.form()
                    .setError("badCode")
                    .createForm(SmsProperties.SMS_FORM_NAME);
            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }
        // 验证成功
        context.success();
    }

    private boolean validateCode(AuthenticationFlowContext context) {

        // 先拿出表单配置
        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
        String enableSms = config.getConfig().getOrDefault(SmsProperties.SMS_ENABLE, "false");
        if ("false".equals(enableSms)) {
            // 未开启手机验证码
            log.warn("手机验证码MFA未开启");
            return true;
        }
        String debugMode = config.getConfig().getOrDefault(SmsProperties.SMS_DEBUG_MODE, "false");
        boolean isDebug = "true".equals(debugMode);
        // 拿到表单数据
        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        // 拿到手机验证码
        String code = formData.getFirst("smsCode");
        if (isDebug && Objects.equals(code, config.getConfig().getOrDefault(SmsProperties.SMS_DEBUG_MODE_VALUE, "8888"))) {
            // 如果是调试模式,并且验证码正确,返回true
            log.warn("当前是debug模式,并且通过万能验证码登录");
            return true;
        }
        // 表单需要传入当前凭据的信息Id
        String credentialId = formData.getFirst("credentialId");
        if (credentialId == null || credentialId.isEmpty()) {
            credentialId = getCredentialProvider(context.getSession())
                    .getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
        }
        log.info("当前的credentialId:{}", credentialId);
        // 可以用过getChallengeResponse获得code,通过credentialId获得手机号凭证
        UserCredentialModel input = new UserCredentialModel(credentialId, this.getType(context.getSession()), code);
        // 这里调用凭证的校验,是否存在以及验证码是否存在
        return this.getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
    }


    /**
     * 由于需要用户交互输入验证,所以这里必须为true,false表示无需用户校验(解析token等方式直接登录)
     */
    @Override
    public boolean requiresUser() {
        return true;
    }

    /**
     * 这个方法用于判断用户是否配置特定的认证器,使用provider中的isConfiguredFor方法
     */
    @Override
    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        return this.getCredentialProvider(keycloakSession).isConfiguredFor(realmModel, userModel, this.getType(keycloakSession));
    }

    /**
     * 如果configuredFor()返回false，并且认证流程中需要我们的验证器，并且仅当关联的AuthenticatorFactory的isUserSetupAllowed方法返回true时
     * setRequiredActions()方法负责注册必须由用户完成的操作
     */
    @Override
    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        // 指定必须操作的ID
        // 用户注册后弹出必须的设置界面
        userModel.addRequiredAction(SmsOptMfaAuthRequiredAction.PROVIDER_ID);
    }

    @Override
    public List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
        return Authenticator.super.getRequiredActions(session);
    }

    @Override
    public boolean areRequiredActionsEnabled(KeycloakSession session, RealmModel realm) {
        return Authenticator.super.areRequiredActionsEnabled(session, realm);
    }

    @Override
    public void close() { }

    @Override
    public SmsCredentialProvider getCredentialProvider(KeycloakSession keycloakSession) {
        // 注意获取provider的方式,需要强转,直接获取会获取不到
        return (SmsCredentialProvider) keycloakSession.getProvider(CredentialProvider.class, SmsCredentialProviderFactory.PROVIDER_ID);
    }
}
